package com.wh.tokenizer;

import com.wh.exception.JsonParseException;

import java.io.IOException;

public class Tokenizer {

    private CharReader charReader;

    private TokenList tokens;

    public TokenList tokenize(CharReader charReader) throws IOException {
        this.charReader = charReader;
        tokens = new TokenList();
        tokenize();

        return tokens;
    }

    private void tokenize() throws IOException {
        // 使用do-while处理空文件
        Token token;
        do {
            token = start();
            tokens.add(token);
        } while (token.getTokenType() != TokenType.END_DOCUMENT);
    }

    private Token start() throws IOException {
        char ch;
        for(;;) {
            if (!charReader.hasMore()) {
                return new Token(TokenType.END_DOCUMENT, null);
            }
            ch = charReader.peek();
            if (!isWhiteSpace(ch)) {
                break;
            }
            charReader.next();
        }

        switch (ch) {
            case '{':
                charReader.next();
                return new Token(TokenType.BEGIN_OBJECT, String.valueOf(ch));
            case '}':
                charReader.next();
                return new Token(TokenType.END_OBJECT, String.valueOf(ch));
            case '[':
                charReader.next();
                return new Token(TokenType.BEGIN_ARRAY, String.valueOf(ch));
            case ']':
                charReader.next();
                return new Token(TokenType.END_ARRAY, String.valueOf(ch));
            case ',':
                charReader.next();
                return new Token(TokenType.SEP_COMMA, String.valueOf(ch));
            case ':':
                charReader.next();
                return new Token(TokenType.SEP_COLON, String.valueOf(ch));
            case 'n':
                return readNull();
            case 't':
            case 'f':
                return readBoolean();
            case '"':
                return readString();
            case '-':
                return readNumber();
        }

        if (isDigit(ch)) {
            return readNumber();
        }

        throw new JsonParseException("Illegal character");
    }

    private boolean isWhiteSpace(char ch) {
        return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
    }

    /*
    不保留转义
     */
    private Token readString() throws IOException {
        // 首个是 "
        charReader.next();
        StringBuilder sb = new StringBuilder();
        while (charReader.hasMore()) {
            char ch = charReader.next();
            if (ch == '"') {
                return new Token(TokenType.STRING, sb.toString());
            } else if (ch == '\\') {
                ch = charReader.next();
                if (ch == '"') sb.append('"');
                else if (ch == '\\') sb.append('\\');
                else if (ch == 'b') sb.append('\b');
                else if (ch == 'f') sb.append('\f');
                else if (ch == 'n') sb.append('\n');
                else if (ch == 'r') sb.append('\r');
                else if (ch == 't') sb.append('\t');
                else if (ch == 'u') {
                    StringBuilder hexDigit = new StringBuilder();
                    for (int i = 0; i < 4; i++) {
                        if (!charReader.hasMore())  throw new JsonParseException("Unicode error : \\u" + hexDigit.toString());
                        ch = charReader.next();
                        if (!isHex(ch)) throw new JsonParseException("Unicode error : \\u" + hexDigit.toString());
                        hexDigit.append(ch);
                    }
                    sb.append(Character.toChars(Integer.parseInt(hexDigit.toString(), 16)));
                } else {
                    throw new JsonParseException("escape character error");
                }
            } else if (ch == '\r' || ch == '\n'){
                throw new JsonParseException("\\r and \\n can't be inside");
            }else {
                sb.append(ch);
            }
        }
        throw new JsonParseException("Illegal character");
    }

    private boolean isHex(char ch) {
        return ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F');
    }

    // 不支持超大数
    private Token readNumber() throws IOException {
        char ch = charReader.next();
        StringBuilder sb = new StringBuilder();
        if (ch == '-') {
            sb.append(ch);
            ch = charReader.next();
        }
        if (ch == '0') {
            sb.append(ch);
            if (charReader.hasMore()) ch = charReader.next();
            else ch = '@';
        } else if (isOne2Nine(ch)) {
            sb.append(ch);
            while (charReader.hasMore() && isDigit(ch = charReader.next())) {
                sb.append(ch);
            }
        } else {
            throw new JsonParseException("Illegal character");
        }
        if (ch == '.') {
            sb.append(ch);
            if (charReader.hasMore() && isDigit(ch = charReader.next())) {
                sb.append(ch);
                while (charReader.hasMore() && isDigit(ch = charReader.next())){
                    sb.append(ch);
                }
            } else {
                throw new JsonParseException("Illegal character");
            }
        }
        if (isExp(ch)) {
            sb.append(ch);
            if (!charReader.hasMore()) {
                throw new JsonParseException("Illegal character");
            }
            ch = charReader.next();
            if (ch == '-' || ch == '+') {
                sb.append(ch);
                if (!charReader.hasMore()) {
                    throw new JsonParseException("Illegal character");
                }
                ch = charReader.next();
            }
            if (isDigit(ch)) sb.append(ch);
            else {
                throw new JsonParseException("Illegal character");
            }
            while (charReader.hasMore() && isDigit(ch = charReader.next())){
                sb.append(ch);
            }
        }
        return new Token(TokenType.NUMBER, sb.toString());
    }

    private boolean isExp(char ch) throws IOException {
        return ch == 'e' || ch == 'E';
    }

    private boolean isDigit(char ch) {
        return ch >= '0' && ch <= '9';
    }

    private boolean isOne2Nine(char ch) {
        return ch >= '1' && ch <= '9';
    }

    private Token readBoolean() throws IOException {
        if (charReader.peek() == 't') {
            charReader.next();
            if (!(charReader.next() == 'r' && charReader.next() == 'u' && charReader.next() == 'e')) {
                throw new JsonParseException("Invalid json string");
            }

            return new Token(TokenType.BOOLEAN, "true");
        } else {
            charReader.next();
            if (!(charReader.next() == 'a' && charReader.next() == 'l'
                    && charReader.next() == 's' && charReader.next() == 'e')) {
                throw new JsonParseException("Invalid json string");
            }

            return new Token(TokenType.BOOLEAN, "false");
        }
    }

    private Token readNull() throws IOException {
        if (!(charReader.next() == 'n' && charReader.next() == 'u' && charReader.next() == 'l' && charReader.next() == 'l')) {
            throw new JsonParseException("Invalid json string");
        }

        return new Token(TokenType.NULL, "null");
    }
}
